--[[
LPEG based parser for winhex template file
panxiaohui,2025-7-16
]]

local rtk = require("libluartk")
local gx = require("libgamex")
local lpeg = require("lpeglabel")

lpeg.locale(lpeg)

local P, S, V, R = lpeg.P, lpeg.S, lpeg.V, lpeg.R
local C, Carg, Cb, Cc = lpeg.C, lpeg.Carg, lpeg.Cb, lpeg.Cc
local Cf, Cg, Cmt, Cp, Cs, Ct = lpeg.Cf, lpeg.Cg, lpeg.Cmt, lpeg.Cp, lpeg.Cs, lpeg.Ct
local Lc, T = lpeg.Lc, lpeg.T

local chinese = lpeg.utfR(0x4e00, 0x9fa5) + lpeg.utfR(0x3400, 0x4db5)
-- 自定义"字母"模式：ASCII字母 + 中文字符
local alpha = lpeg.alpha + chinese
local alnum = alpha + lpeg.digit
local digit = lpeg.digit
local xdigit = lpeg.xdigit
local space = lpeg.space

local labels = {
	{ "ErrDigitHex", "Expected hex digits" },
	{ "ErrBegin", "Expect begin" },
	{ "ErrEnd", "Expect end" },
	{ "ErrLeftPar", 'Expect "("' },
	{ "ErrRightPar", 'Expect ")"' },
	{ "Err_1248", "Expect integer width specifier:1/2/4 or 8" },
	{ "ErrString", "Expect string" },
	{ "ErrIntType", "Expect integer type spec." },
	{ "ErrInt", "Expect integer" },
	{ "fail", "unknown error" },
}

rtk.table_ieach(labels, function(k, v)
	labels[v[1]] = v
end)

local function throw(label)
	label = "Err" .. label
	for i, labelinfo in ipairs(labels) do
		if labelinfo[1] == label then
			return T(i)
		end
	end
	error("Label not found: " .. label)
end

local function expect(patt, label)
	return patt + throw(label)
end

local id = C((alpha + "_") * (alnum + "_") ^ 0)
local comment = (P("//") * (P(1) - "\n") ^ 0 * "\n") + (P("#") * (P(1) - "\n") ^ 0 * "\n")
local sp = (S(" \t\n\r") ^ 1 + comment) ^ 0
local str = P('"') * C((1 - P('"')) ^ 0) * P('"') * sp
local semicolon = P(";") * sp
local comma = P(",") * sp
local colon = P(":") * sp
local brace_left = P("{") * sp
local brace_right = P("}") * sp
local hex = C((P("0x") + "0X") * expect(xdigit ^ 1, "DigitHex")) * sp / tonumber
local decimal = C((S("+-") ^ 0) * digit ^ 1) * sp / tonumber
local int = hex + decimal

-- regular combinators and auxiliary functions

local function token(patt)
	return patt * sp
end

local function tokenc(patt)
	return C(patt) * sp
end

local function kw(str)
	return token(P(str) * -V("IdRest"))
end

-- keyword with Capture
local function kwc(str)
	return token(P(str) * -V("IdRest")) / str
end

local function dec(n)
	return n - 1
end

local function tagC(tag, patt)
	return Ct(Cg(Cp(), "pos") * Cg(Cc(tag), "tag") * patt * Cg(Cp() / dec, "end_pos"))
end

local function tag(tag, field)
	return Cg(Cc(tag), field)
end

local function sepBy(patt, sep, label)
	if label then
		return patt * Cg(sep * expect(patt, label)) ^ 0
	else
		return patt * Cg(sep * patt) ^ 0
	end
end

local global_types = {}
local function add_type(tp)
	--	printk(2,'Define %s=%s\n',tp.name,json.encode(tp))
	global_types[tp.name] = tp
end

local G =
	{
		"TypeList",
		Id = tokenc(-V("Reserved") * C(V("Ident"))),
		Reserved = V("Keywords") * -V("IdRest"),
		Keywords = P("template")
			+ P("begin")
			+ P("end")
			+ P("section")
			+ P("endsection")
			+ P("goto")
			+ P("enum")
			+ "default_width",
		Type = V("IntType") + kwc("hex") + V("Name"),
		IntType = kwc("bool") + kwc("char") + kwc("uint8") + kwc("int16") + kwc("uint16") + kwc("int32") + kwc(
			"uint32"
		) + kwc("int64") + kwc("uint64"),
		Ident = V("IdStart") * V("IdRest") ^ 0,
		IdStart = alpha + P("_"),
		IdRest = alnum + P("_"),
		Name = V("Id") + str,
		TypeDecl = (V("Struct") + V("Enum")) / function(cap)
			add_type(cap)
			return cap
		end,
		Struct = Ct(
			sp
				* kw("struct")
				* Cg(Cc("Struct"), "type")
				* Cg(V("Name"), "name")
				* expect(kw("begin"), "Begin")
				* Cg(V("Body"), "items")
				* expect(kw("end"), "End")
		),
		Enum = Ct(
			sp
				* kw("enum")
				* Cg(token("(") * expect(V("IntType"), "IntType") * expect(token(")"), "RightPar"), "base_type") ^ 0
				* Cg(Cc("Enum"), "type")
				* Cg(V("Name"), "name")
				* expect(kw("begin"), "Begin")
				* Cg(V("EnumBody"), "items")
				* expect(kw("end"), "End")
		),
		EnumBody = Ct(V("EnumItem") ^ 1),
		EnumItem = Ct(Cg(str, "name") * Cg(token("=") * int, "value") ^ 0),
		TypeList = Ct(V("TypeDecl") ^ 1) * -1,
		Body = Ct(V("Item") ^ 1),
		Item = V("BasicItem") + V("ArrayItem") + V("HexItem") + V("SectionItem") + V("Goto") + V("Skip"),
		BasicItem = Ct(Cg(V("Type"), "type") * Cg(V("Name"), "name")),
		HexItem = kw("hex") * Ct(Cg(Cc("hex"), "type") * Cg(int, "size") * Cg(V("Name"), "name")),
		ArrayItem = V("BasicArray") + V("SectionArray"),
		BasicArray = Ct(
			Cg(Cc("Array"), "type")
				* Cg(V("Type"), "ele_type")
				* token("[")
				* Cg(int, "size")
				* token("]")
				* Cg(V("Name"), "name")
		),
		SectionArray = Ct(
			Cg(Cc("SecArray"), "type")
				* kw("section")
				* Cg(V("Name"), "name")
				* token("{")
				* Cg(Ct(V("Item") ^ 1), "items")
				* token("}")
				* token("[")
				* Cg(int, "size")
				* token("]")
		),
		SectionItem = Ct(
			Cg(Cc("Struct"), "type")
				* kw("section")
				* Cg(V("Name"), "name")
				* Cg(Ct(V("Item") ^ 1), "items")
				* kw("endsection")
		),
		Goto = kw("goto") * Ct(Cg(Cc("goto"), "type") * Cg(int, "offset")),
		Skip = kw("skip") * Ct(Cg(Cc("skip"), "type") * Cg(expect(int, "Int"), "offset")),
	}

local function lineno(s, i)
	if i == 1 then
		return 1, 1
	end
	local l, lastline = 0, ""
	s = s:sub(1, i) .. "\n"
	for line in s:gmatch("[^\n]*[\n]") do
		l = l + 1
		lastline = line
	end
	local c = lastline:len() - 1
	return l, c ~= 0 and c or 1
end

-- creates an error message for the input string
local function syntaxerror(errorinfo, pos, msg)
	local l, c = lineno(errorinfo.subject, pos)
	local error_msg = "%s:%d:%d: syntax error, %s"
	return string.format(error_msg, errorinfo.filename, l, c, msg)
end

local function parse(parser, subject, filename)
	local errorinfo = { subject = subject, filename = filename }
	lpeg.setmaxstack(1000)
	local ast, label, errorpos = lpeg.match(G, subject, nil, errorinfo)
	if not ast then
		if parser.detailed_errors then
			local re = require("relabel")
			local line, col = re.calcline(subject, errorpos)
			return ast,
				{
					line = line,
					column = col,
					id = labels[label][1],
					message = labels[label][2],
					position = errorpos,
				}
		else
			local errmsg = labels[label][2]
			return ast, syntaxerror(errorinfo, errorpos, errmsg)
		end
	end
	return ast
end

local M = {}

function M.parse(str, file)
	return parse(parser, str, file or "unamed script")
end

function M.parse_file(file)
	local rc
	local parser = { detailed_errors = false }
	local fh = io.open(file, "rt")
	if not fh then
		rtk.printk(2, "Error open(%s)\n", file)
		return
	end
	rc = fh:read("*a")
	if not rc then
		rtk.printk(2, "Error read(%s)\n", file)
		fh:close()
		return
	end
	local ast, err = parse(parser, rc, file)
	if not ast then
		rtk.printk(2, "%s\n", err)
		return
	end
	return ast, err
end

local FS = "    "
local function str_normalize(name)
	return name:gsub("[^a-zA-Z0-9_]", "_")
end
local function basic_type(t)
	local types = {
		bool = { type = "bool", FSize = 1 },
		char = { gx.read_char, gx.write_char, FSize = 1, type = "char" },
		int8 = { gx.read_int8, gx.write_int8, FSize = 1, type = "int8" },
		uint8 = { gx.read_uint8, gx.write_uint8, FSize = 1, type = "uint8" },
		int16 = { gx.read_int16, gx.write_int16, FSize = 2, type = "int16" },
		uint16 = { gx.read_uint16, gx.write_uint16, FSize = 2, type = "uint16" },
		int32 = { gx.read_int32, gx.write_int32, FSize = 4, type = "int32" },
		uint32 = { gx.read_uint32, gx.write_uint32, FSize = 4, type = "uint32" },
		int64 = { gx.read_int64, gx.write_int64, FSize = 8, type = "int64" },
		uint64 = { gx.read_uint64, gx.write_uint64, FSize = 8, type = "uint64" },
		hex = { type = "hex" },
		string = { type = "string" },
	}
	return types[t]
end
local function add_code(str, ctable)
	--print('code:',str)
	table.insert(ctable, str)
end

--[[
语法树正规化
]]
function M.compile2(ast)
	local code = {}
	local str
	--	printk(2,'=== name(%s),type(%s)\n',ast.name,ast.type)
	if ast.type == "Enum" then
		ast.base_type = ast.base_type or "int32"
		local bt = basic_type(ast.base_type)
		assert(bt, string.format('Invalid base type for enum "%s"', ast.name))
		ast.FSize = bt.FSize
		local values = {}
		local labels = {}
		local last_val = 0
		rtk.table_ieach(ast.items, function(_, it)
			last_val = it.value or last_val
			table.insert(values, last_val)
			--			table.insert(labels,it.name)
			table.insert(labels, string.format("%03d-%s", last_val, it.name))
			last_val = last_val + 1
		end)
		ast.labels = labels
		ast.values = values
		return
	end
	ast.FSize = 0
	local scopes = { ast }
	local function findType(name)
		local tp = basic_type(name)
		if tp then
			return tp
		end
		for i = #scopes, 1, -1 do
			local scp = scopes[i]
			tp = scp[name]
			if tp then
				break
			end
		end
		if not tp then
			tp = global_types[name]
		end
		local btype
		if tp and tp.type == "Enum" then
			btype = tp.base_type or "int32"
		end
		return tp, btype
	end
	local function handle_item(it, parent)
		local str
		it.FOffset = parent.FPtr or 0
		if it.type == "Array" then
			if it.ele_type == "char" then
				-- array of char is special
				it.type = basic_type("string")
				it.FSize = it.size
				it.ele_type = nil
			else
				local etp = findType(it.ele_type)
				assert(etp, string.format('Unknown ele_type : "%s"', it.ele_type))
				assert(etp.FSize, string.format('Size of type "%s" unknown.', it.ele_type))
				it.FSize = etp.FSize * it.size
				it.type = {
					type = "Array",
					name = "arr_" .. it.name,
					ele_type = etp,
					FSize = it.FSize,
					size = it.size,
					FElementSize = etp.FSize,
				}
			end
			parent.FPtr = (parent.FPtr or 0) + it.FSize
		elseif it.type == "goto" then
			-- assert(parent.FPtr or 0 <= it.offset)
			parent.FPtr = it.offset
			it.FSize = 0
		elseif it.type == "skip" then
			parent.FPtr = parent.FPtr + it.offset
			it.FSize = 0
		elseif it.type == "Struct" then
			local struct_name = "Struct_" .. it.name
			local typeDef = { type = "Struct", name = struct_name, FSize = 0, items = it.items }
			it.items = nil
			table.insert(scopes, typeDef)
			rtk.table_ieach(typeDef.items, function(_, sit)
				handle_item(sit, typeDef)
			end)
			parent.FPtr = (parent.FPtr or 0) + typeDef.FSize
			it.type = typeDef
			it.FSize = typeDef.FSize
			parent.subtypes = parent.subtypes or {}
			parent.subtypes[struct_name] = typeDef
		elseif it.type == "SecArray" then
			local subtype = {}
			subtype.name = "Struct_" .. it.name
			subtype.type = "Struct"
			subtype.FSize = 0
			subtype.items = it.items
			table.insert(scopes, typeDef)
			rtk.table_ieach(it.items, function(_, sit)
				handle_item(sit, subtype)
			end)
			table.remove(scopes)
			it.items = nil
			it.FSize = subtype.FSize * it.size
			parent.FPtr = (parent.FPtr or 0) + it.FSize
			it.type = {
				type = "Array",
				name = "arr_" .. it.name,
				ele_type = subtype,
				FSize = it.FSize,
				size = it.size,
				FElementSize = subtype.FSize,
			}
			parent.subtypes = parent.subtypes or {}
			parent.subtypes[subtype.name] = subtype
		else
			local btype = findType(it.type)
			assert(btype, string.format('Unknown type : "%s" of "%s"', it.type, it.name))
			if it.type == "hex" then
				it.FSize = it.size
			else
				it.FSize = btype.FSize
			end
			parent.FPtr = (parent.FPtr or 0) + it.FSize
			it.type = btype
		end
		parent.FSize = (parent.FPtr > parent.FSize) and parent.FPtr or parent.FSize
		if it.name then
			parent.items[it.name] = it
		end
		return it
	end
	rtk.table_ieach(ast.items, function(_, it)
		local newit = handle_item(it, ast)
		ast.items[_] = newit
	end)
	return ast
end

--[[
Convert tpl to C++ structure
]]
function M.compile_to_cxx(ast)
	local code = {}
	local str
	local sname = str_normalize(ast.name)
	str = string.format("struct %s{", sname)
	add_code(str, code)
	local function handle_item(it, parent, istr)
		local str
		sname = str_normalize(it.name or "")
		local btype = basic_type(it.type)
		if btype then
			str = string.format("%s%s %s;", istr, btype[1], it.name)
		elseif it.type == "hex" then
			str = string.format("%suint8_t %s[%d];", istr, it.name, it.size)
		elseif it.type == "Array" then
			btype = basic_type(it.ele_type)
			assert(btype, string.format("Unknown ele_type : %s", it.ele_type))
			str = string.format("%s%s %s[%d];", istr, btype[1], it.name, it.size)
			it.FSize = btype[2] * it.size
			parent.FPtr = (parent.FPtr or 0) + it.FSize
		elseif it.type == "goto" then
			str = string.format("%schar padding_0x%x[0x%x];", istr, it.offset, it.FSize)
		elseif it.type == "Struct" then
			local scode = {}
			local sname = str_normalize(it.name)
			add_code(string.format("%sstruct _tag_%s{", istr, sname), scode)
			rtk.table_ieach(it.items, function(_, sit)
				local sstr = handle_item(sit, it, istr .. FS)
				if sstr then
					add_code(sstr, scode)
				else
					error("unexpected item")
				end
			end)
			add_code(string.format("%s}%s;", istr, sname), scode)
			str = table.concat(scode, "\n")
		elseif it.type == "SecArray" then
			local scode = {}
			local sname = str_normalize(it.name)
			add_code(string.format("%sstruct _tag_%s{", istr, sname), scode)
			it.FSize = 0
			rtk.table_ieach(it.items, function(_, sit)
				local sstr = handle_item(sit, it, istr .. FS)
				if sstr then
					add_code(sstr, scode)
				else
					rtk.jprint(sit)
				end
			end)
			add_code(string.format("%s}%s[%d];", istr, sname, it.size), scode)
			str = table.concat(scode, "\n")
			it.FElementSize = it.FSize
			it.FSize = it.FSize * it.size
			parent.FPtr = (parent.FPtr or 0) + it.FSize
		else
			rtk.jprint(it)
		end
		if str then
			str = str .. string.format("// 0x%x", it.FOffset)
		end
		return str
	end
	rtk.table_ieach(ast.items, function(_, it)
		local line = handle_item(it, ast, FS)
		if line then
			add_code(line, code)
		end
	end)
	add_code("}", code)
	return table.concat(code, "\n")
end

function M.load_winhex_template(file)
	local types = M.parse_file(file)
	assert(types, "Parsing template failed.")
	local _, ast
	for _, ast in ipairs(types) do
		types[ast.name] = M.compile2(ast)
	end
	return types
end

return M
